Passed
Pull Request — master (#2)
by Muhammad Dyas
01:30
created

ActionHandler.startPoll   F

Complexity

Conditions 27

Size

Total Lines 62
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 45
dl 0
loc 62
rs 0
c 0
b 0
f 0
cc 27

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like ActionHandler.startPoll often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import {chat_v1 as chatV1} from 'googleapis/build/src/apis/chat/v1';
2
import BaseHandler from './BaseHandler';
3
import NewPollFormCard from '../cards/NewPollFormCard';
4
import {addOptionToState} from '../helpers/option';
5
import {callMessageApi} from '../helpers/api';
6
import {createDialogActionResponse, createStatusActionResponse} from '../helpers/response';
7
import PollCard from '../cards/PollCard';
8
import {PollState, Voter, Votes} from '../helpers/interfaces';
9
import AddOptionFormCard from '../cards/AddOptionFormCard';
10
import {saveVotes} from '../helpers/vote';
11
import {MAX_NUM_OF_OPTIONS} from '../config/default';
12
13
/*
14
This list methods are used in the poll chat message
15
 */
16
interface PollAction {
17
  saveOption(): Promise<chatV1.Schema$Message>;
18
19
  getEventPollState(): PollState;
20
}
21
22
export default class ActionHandler extends BaseHandler implements PollAction {
23
  constructor(event: chatV1.Schema$DeprecatedEvent) {
24
    super(event);
25
  }
26
27
  async process(): Promise<chatV1.Schema$Message> {
28
    const action = this.event.common?.invokedFunction;
29
    switch (action) {
30
      case 'start_poll':
31
        return await this.startPoll();
32
      case 'vote':
33
        return this.recordVote();
34
      case 'add_option_form':
35
        return this.addOptionForm();
36
      case 'add_option':
37
        return await this.saveOption();
38
      case 'show_form':
39
        return {
40
          actionResponse: {
41
            type: 'DIALOG',
42
            dialogAction: {
43
              dialog: {
44
                body: new NewPollFormCard({topic: '', choices: []}).create(),
45
              },
46
            },
47
          },
48
        };
49
      default:
50
        return createStatusActionResponse('Unknown action!', 'UNKNOWN');
51
    }
52
  }
53
54
  /**
55
   * Handle the custom start_poll action.
56
   *
57
   * @param {object} chatV1.Schema$DeprecatedEvent - chat event
58
   * @returns {object} Response to send back to Chat
59
   */
60
  async startPoll() {
61
    // Get the form values
62
    const formValues = this.event.common?.formInputs;
63
    const topic = formValues?.['topic']?.stringInputs?.value?.[0]?.trim() ?? '';
64
    const isAnonymous = formValues?.['is_anonymous']?.stringInputs?.value?.[0] === '1';
65
    const allowAddOption = formValues?.['allow_add_option']?.stringInputs?.value?.[0] === '1';
66
    const choices = [];
67
    const votes: Votes = {};
68
69
    for (let i = 0; i < MAX_NUM_OF_OPTIONS; ++i) {
70
      const choice = formValues?.[`option${i}`]?.stringInputs?.value?.[0]?.trim();
71
      if (choice) {
72
        choices.push(choice);
73
        votes[i] = [];
74
      }
75
    }
76
77
    if (!topic || choices.length === 0) {
78
      // Incomplete form submitted, rerender
79
      const dialog = new NewPollFormCard({
80
        topic,
81
        choices,
82
      }).create();
83
      return {
84
        actionResponse: {
85
          type: 'DIALOG',
86
          dialogAction: {
87
            dialog: {
88
              body: dialog,
89
            },
90
          },
91
        },
92
      };
93
    }
94
    const pollCard = new PollCard({
95
      topic: topic, choiceCreator: undefined,
96
      author: this.event.user,
97
      choices: choices,
98
      votes: votes,
99
      anon: isAnonymous,
100
      optionable: allowAddOption,
101
    }).createCardWithId();
102
    // Valid configuration, make the voting card to display in the space
103
    const message = {
104
      cardsV2: [pollCard],
105
    };
106
    const request = {
107
      parent: this.event.space?.name,
108
      requestBody: message,
109
    };
110
    const apiResponse = await callMessageApi('create', request);
111
    if (apiResponse) {
112
      return createStatusActionResponse('Poll started.', 'OK');
113
    } else {
114
      return createStatusActionResponse('Failed to start poll.', 'UNKNOWN');
115
    }
116
  }
117
118
  /**
119
   * Handle the custom vote action. Updates the state to record
120
   * the user's vote then rerenders the card.
121
   *
122
   * @param {object} event - chat event
123
   * @returns {object} Response to send back to Chat
124
   */
125
  recordVote() {
126
    const parameters = this.event.common?.parameters;
127
    if (!(parameters?.['index'])) {
128
      throw new Error('Index Out of Bounds');
129
    }
130
    const choice = parseInt(parameters['index']);
131
    const userId = this.event.user?.name ?? '';
132
    const userName = this.event.user?.displayName ?? '';
133
    const voter: Voter = {uid: userId, name: userName};
134
    const state = JSON.parse(parameters['state']);
135
136
    // Add or update the user's selected option
137
    state.votes = saveVotes(choice, voter, state.votes, state.anon);
138
    const card = new PollCard(state);
139
    return {
140
      thread: this.event.message?.thread,
141
      actionResponse: {
142
        type: 'UPDATE_MESSAGE',
143
      },
144
      cardsV2: [card.createCardWithId()],
145
    };
146
  }
147
148
  /**
149
   * Opens and starts a dialog that allows users to add details about a contact.
150
   *
151
   * @param {object} event the event object from Google Chat.
152
   *
153
   * @returns {object} open a dialog.
154
   */
155
  addOptionForm() {
156
    const card = this.event.message!.cardsV2?.[0]?.card;
157
    // @ts-ignore: because too long
158
    const stateJson = (card.sections[0].widgets[0].decoratedText?.button?.onClick?.action?.parameters[0].value || card.sections[1].widgets[0].decoratedText?.button?.onClick?.action?.parameters[0].value) ?? '';
159
    const state = JSON.parse(stateJson);
160
    const dialog = new AddOptionFormCard(state).create();
161
    return createDialogActionResponse(dialog);
162
  };
163
164
  /**
165
   * Handle add new option input to the poll state
166
   * the user's vote then rerenders the card.
167
   *
168
   * @returns {object} Response to send back to Chat
169
   */
170
  async saveOption(): Promise<chatV1.Schema$Message> {
171
    const userName = this.event.user?.displayName ?? '';
172
    const state = this.getEventPollState();
173
    const formValues = this.event.common?.formInputs;
174
    const optionValue = formValues?.['value']?.stringInputs?.value?.[0]?.trim() || '';
175
    addOptionToState(optionValue, state, userName);
176
177
    const cardMessage = new PollCard(state).createMessage();
178
179
    const request = {
180
      name: this.event.message!.name,
181
      requestBody: cardMessage,
182
      updateMask: 'cardsV2',
183
    };
184
    const apiResponse = await callMessageApi('update', request);
185
    if (apiResponse) {
186
      return createStatusActionResponse('Option is added', 'OK');
187
    } else {
188
      return createStatusActionResponse('Failed to add option.', 'UNKNOWN');
189
    }
190
  }
191
192
  getEventPollState(): PollState {
193
    const parameters = this.event.common?.parameters;
194
    const state = parameters?.['state'];
195
    if (!state) {
196
      throw new ReferenceError('no valid state in the event');
197
    }
198
    return JSON.parse(state);
199
  }
200
}
201